﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Axial;
using System.Text;
using Axial.Reflection;

namespace Axial
{
	/// <summary>
	/// Class that takes methods and makes them runnable in a browser
	/// </summary>
	public static class JSWebTranslator
	{
		static JSWebTranslator()
		{
		}

		public static JSWriter DissembleMethod(MethodBase methodInfo, params object[] paramArray)
		{
			MethodBodyReader bodyReader = new MethodBodyReader(methodInfo);
			Stack stack = new Stack();

			List<ILInstruction> instructions = bodyReader.Instructions;
			instructions = ILOptimizer.Optimize(instructions);

			JSWriter output = new JSWriter();
			// TODO - handle closure as a local
			output.AddStatements(instructions
				.Where(il => il.IsStoreCode() && il.GetStoreLocation() != -1)
				.Select(il => il.GetStoreLocation())
				.Distinct()
				.Select(i => "var " + GetLocalVariableName(i)));
			output.AddStatements(DissembleInstructions(instructions, paramArray, stack));
			return output;
		}

		public static JSWriter DissembleAction(object function)
		{
			JSWriter output = new JSWriter();
			Delegate fxn = (Delegate)function;
			List<object> parms = new List<object> { fxn.Target };
			parms.AddRange(Enumerable.Range(0, fxn.Method.GetParameters().Length).Select(i => (object)("arg" + i)));
			JSWriter code = DissembleMethod(fxn.Method, parms.ToArray());
			output.AddAnonymousFunction(fxn.Method.GetParameters().Length, code);
			return output;
		}

		internal static JSWriter DissembleInstructions(List<ILInstruction> instructions, object[] paramArray, Stack stack)
		{
			JSWriter output = new JSWriter();

			if (instructions.Count == 0)
				return output;

			Dictionary<int, List<int>> flows = DetectFlow(instructions, paramArray);

			var codestatement = FormCodeSegment(instructions, paramArray);
			return codestatement.ToJSWriter();
		}

		private static CodeSegment FormCodeSegment(List<ILInstruction> instructions, object[] paramArray)
		{
			List<CodeStatement> pieces = new List<CodeStatement>();
			var chopmeup = instructions.AsEnumerable();
			foreach (ILInstruction inst in chopmeup.Where(il => il.IsStatement))
			{
				var piece = chopmeup.Take(chopmeup.IndexOf(inst) + 1).ToArray();
				CodeStatement statement = CodeStatement.Create(piece, paramArray);
				pieces.Add(statement);
				chopmeup = chopmeup.Skip(piece.Length);
			}
			CodeSegment segment = new CodeSegment(pieces);
			segment.RemoveEndingReturn();
			return segment;
		}

		private static Dictionary<int, List<int>> DetectFlow(List<ILInstruction> instructions, object[] paramArray)
		{
			Dictionary<int, List<int>> flows = new Dictionary<int, List<int>>();
			foreach (var inst in instructions)
				flows[inst.Offset] = new List<int>();
			for (int i = 0; i < instructions.Count - 1; ++i)
			{
				var inst = instructions[i];
				if (inst.IsConditionalBranch())
				{
					flows[inst.Offset].Add(instructions[i + 1].Offset);
					flows[inst.Offset].Add(inst.GetBranchTarget());
				}
				else if (inst.IsBranch())
					flows[inst.Offset].Add(inst.GetBranchTarget());
				else
					flows[inst.Offset].Add(instructions[i + 1].Offset);
			}

			return flows;
		}

		internal static string GetLocalVariableName(int endingNumber)
		{
			return new string((char)('a' + endingNumber), 1);
		}

		internal static int GetEndingNumber(ILInstruction inst)
		{
			int index = (int)char.GetNumericValue(inst.Code.Name, inst.Code.Name.Length - 1);
			return index;
		}

		internal static List<object> GetParametersFromStack(Stack stack, MethodBase method)
		{
			List<object> pops = new List<object>();
			foreach (var param in Enumerable.Range(0, method.GetParameters().Count()))
				pops.Add(stack.Pop());
			pops.Reverse();
			return pops;
		}

		internal static string SanitizeIdentifier(string identifier)
		{
			return identifier.Replace("<", "").Replace(">", "");
		}

		public static string DissembleClass(Type type)
		{
			JSWriter codeWriter = new JSWriter();
			foreach (FieldInfo field in type.GetFields())
				codeWriter.AddPublicField(field.Name, field.FieldType.GetDefaultValue());
			foreach (FieldInfo field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
				codeWriter.AddPrivateField(field.Name, field.FieldType.GetDefaultValue());

			foreach (FieldInfo field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static))
				codeWriter.AddStaticField(field.Name, field.FieldType.GetDefaultValue());

			foreach (MethodInfo method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance))
			{
				var paramObj = from i in Enumerable.Range(0, method.GetParameters().Length)
							   select (object)("arg" + i);
				if (!method.IsStatic)
					paramObj = new object[] { "this" }.Concat(paramObj);
				codeWriter.AddPublicFunction(method.Name, method.GetParameters().Length, DissembleMethod(method, paramObj.ToArray()));
			}
			foreach (MethodInfo method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static))
			{
				var paramObj = from i in Enumerable.Range(0, method.GetParameters().Length)
							   select (object)("arg" + i);
				if (!method.IsStatic)
					paramObj = new object[] { "this" }.Concat(paramObj);
				codeWriter.AddPublicFunction(method.Name, method.GetParameters().Length, DissembleMethod(method, paramObj.ToArray()));
			}

			if (type.GetConstructors().Length > 0)
				codeWriter.AddBlankLine();
			foreach (ConstructorInfo constructor in type.GetConstructors())
			{
				//TODO: implement multiple constructors based on arguments
				var paramObj = from i in Enumerable.Range(0, constructor.GetParameters().Length)
							   select (object)("arguments[" + i + "]");
				paramObj = new object[] { "this" }.Concat(paramObj);
				var paramTypes = constructor.GetParameters().Select(pi => pi.ParameterType);
				codeWriter.AddConstructor(paramTypes.ToArray(), DissembleMethod(constructor, paramObj.ToArray()));
			}

			JSWriter output = new JSWriter();
			output.AddPrivateFunction(type.Name, 0, codeWriter);
			return output.ToString();
		}

		internal static string HandleInstruction(ILInstruction inst, object[] paramArray, Stack stack)
		{
			// TODO: change this to an extension method on ilinstruction?
			if (inst.Code == OpCodes.Nop)
			{
			}
			else if (inst.Code == OpCodes.Ret)
			{
				if (stack.Count == 0)
					return "return";

				object returnValue = stack.Pop();
				// force a true or false in JS mode
				if (paramArray.Length >= 1 && paramArray[0] is System.Web.UI.Page)
				{
					if (returnValue is int && (int)returnValue == 1)
						return "return true";
					return "return " + returnValue + " ? " + returnValue + " : false";
				}
				else
					return "return " + returnValue;
			}
			else if (inst.IsStoreCode() && inst.GetStoreLocation() != -1)
			{
				string var = GetLocalVariableName(inst.GetStoreLocation());
				return var + " = " + (stack.Pop() ?? "null");
			}
			else if (inst.Code == OpCodes.Stfld)
			{
				object value = stack.Pop() ?? "null";
				object called = stack.Pop();
				FieldInfo field = (FieldInfo)inst.Operand;

				// HACK -- jssafe doesn't apply to page class, only codebehind
				if (called is System.Web.UI.Page)
					return "window." + field.Name + " = " + value;
				else if (field.DeclaringType.IsJSSafe() || called is string)
					return string.Format("{0}.{1} = {2}", called, SanitizeIdentifier(field.Name), value);
				else
					field.SetValue(called, value);
			}
			else if (inst.Code == OpCodes.Stsfld)
			{
				object value = stack.Pop();
				FieldInfo field = (FieldInfo)inst.Operand;
				if (field.DeclaringType.IsJSSafe())
					return string.Format("this.constructor.{0} = {1}", SanitizeIdentifier(field.Name), value);
				else
					field.SetValue(null, value);
			}
			else if (inst.Code == OpCodes.Starg_S)
			{
				return paramArray[(int)(byte)inst.Operand] + " = " + stack.Pop();
			}
			else if (inst.Code == OpCodes.Ldarg_S || inst.Code == OpCodes.Ldarga_S)
			{
				int index = (int)(byte)inst.Operand;
				stack.Push(paramArray[index]);
			}
			else if (inst.Code.Name.StartsWith("ldarg."))
			{
				int index = GetEndingNumber(inst);
				stack.Push(paramArray[index]);
			}
			else if (inst.Code == OpCodes.Ldc_R8)
			{
				stack.Push((double)inst.Operand);
			}
			else if (inst.Code == OpCodes.Ldc_R4)
			{
				stack.Push((float)inst.Operand);
			}
			else if (inst.Code == OpCodes.Ldloc_S)
			{
				stack.Push(GetLocalVariableName((int)(byte)inst.Operand));
			}
			else if (inst.Code.Name.StartsWith("ldloc."))
			{
				stack.Push(GetLocalVariableName(GetEndingNumber(inst)));
			}
			else if (inst.Code == OpCodes.Ldc_I4)
			{
				stack.Push(inst.Operand);
			}
			else if (inst.Code == OpCodes.Ldc_I4_M1)
			{
				stack.Push(-1);
			}
			else if (inst.Code == OpCodes.Ldstr)
			{
				stack.Push("'" + ((string)inst.Operand).Replace("\'", "\\\'") + "'");
			}
			else if (inst.Code == OpCodes.Ldloca_S)
			{
				stack.Push(GetLocalVariableName((int)(byte)inst.Operand));
			}
			else if (inst.Code == OpCodes.Ldftn)
			{
				MethodInfo functionInfo = (MethodInfo)inst.Operand;
				stack.Push(functionInfo.MethodHandle.GetFunctionPointer());
			}
			else if (inst.Code == OpCodes.Ldvirtftn)
			{
				MethodInfo functionInfo = (MethodInfo)inst.Operand;
				// HACK: ignore that this function is virtual
				stack.Pop();
				stack.Push(functionInfo.MethodHandle.GetFunctionPointer());
			}
			else if (inst.Code == OpCodes.Ldfld)
			{
				FieldInfo fieldInfo = (FieldInfo)inst.Operand;
				object target = stack.Pop();
				// HACK? process axial objects and webcontrol fields but otherwise spit out the javascript field
				if (target is string)
					stack.Push((string)target + "." + SanitizeIdentifier(fieldInfo.Name));
				else if (target.GetType().Namespace.StartsWith("Axial") || typeof(System.Web.UI.Control).IsAssignableFrom(fieldInfo.FieldType))
					stack.Push(fieldInfo.GetValue(target));
				else
				{
					if (target is System.Web.UI.Page)
						target = "window";
					stack.Push((string)target + "." + SanitizeIdentifier(fieldInfo.Name));
				}
			}
			else if (inst.Code == OpCodes.Ldflda)
			{
				FieldInfo fieldInfo = (FieldInfo)inst.Operand;
				object target = stack.Pop();
				stack.Push("window." + SanitizeIdentifier(fieldInfo.Name));
			}
			else if (inst.Code == OpCodes.Ldsfld)
			{
				FieldInfo fieldInfo = (FieldInfo)inst.Operand;
				if (fieldInfo.DeclaringType.IsJSSafe())
					stack.Push("this.constructor." + SanitizeIdentifier(fieldInfo.Name));
				else
					stack.Push(fieldInfo.GetValue(null));
			}
			else if (inst.Code == OpCodes.Ldc_I4_S)
			{
				stack.Push(Convert.ToInt32(inst.Operand));
			}
			else if (inst.Code.Name.StartsWith("ldc.i4."))
			{
				stack.Push(GetEndingNumber(inst));
			}
			else if (inst.Code == OpCodes.Ldnull)
			{
				stack.Push(null);
			}
			else if (inst.Code == OpCodes.Dup)
			{
				stack.Push(stack.Peek());
			}
			else if (inst.Code == OpCodes.Pop)
			{
				// we've probably discarded the result of a method chain
				return stack.Pop().ToString();
			}
			else if (inst.Code == OpCodes.Add || inst.Code == OpCodes.Add_Ovf || inst.Code == OpCodes.Add_Ovf_Un)
			{
				string arg2 = stack.Pop().ToString();
				string arg1 = stack.Pop().ToString();
				if (arg1.Length != 1)
					arg1 = "(" + arg1 + ")";
				if (arg2.Length != 1)
					arg2 = "(" + arg2 + ")";
				stack.Push(string.Format(arg1 + " + " + arg2));
			}
			else if (inst.Code == OpCodes.Sub || inst.Code == OpCodes.Sub_Ovf || inst.Code == OpCodes.Sub_Ovf_Un)
			{
				string arg2 = stack.Pop().ToString();
				string arg1 = stack.Pop().ToString();
				if (arg1.Length != 1)
					arg1 = "(" + arg1 + ")";
				if (arg2.Length != 1)
					arg2 = "(" + arg2 + ")";
				stack.Push(string.Format(arg1 + " - " + arg2));
			}
			else if (inst.Code == OpCodes.Mul)
			{
				string arg2 = stack.Pop().ToString();
				string arg1 = stack.Pop().ToString();
				stack.Push(string.Format("(" + arg1 + ") * (" + arg2 + ")"));
			}
			else if (inst.Code == OpCodes.Div)
			{
				string arg2 = stack.Pop().ToString();
				string arg1 = stack.Pop().ToString();
				stack.Push(string.Format("(" + arg1 + ") / (" + arg2 + ")"));
			}
			else if (inst.Code == OpCodes.Callvirt || inst.Code == OpCodes.Call)
			{
				//HACK?: check for System.Object constructor -- dunno why this happens
				if (inst.Operand is ConstructorInfo && ((ConstructorInfo)inst.Operand).DeclaringType == typeof(object))
				{
					stack.Pop();
					return null;
				}

				MethodInfo method = (MethodInfo)inst.Operand;
				string code = HandleMethod(stack, inst);
				if (code == null)
					throw new NotImplementedException("The " + method.Name + " method on " + method.DeclaringType.Name + " was not handled.");

				// methods that return void are output, otherwise push it onto the stack
				if (method.ReturnType == typeof(void))
					return code;
				else
					stack.Push(code);
			}
			else if (inst.Code == OpCodes.Initobj)
			{
				return stack.Pop().ToString() + " = new " + (inst.Operand as Type).Name + "()";
			}
			else if (inst.Code == OpCodes.Newobj)
			{
				ConstructorInfo constructor = (ConstructorInfo)inst.Operand;
				object[] param = GetParametersFromStack(stack, constructor).ToArray();
				object newObj;

				// check for closure (use anonymous class)
				if (constructor.DeclaringType.Name.StartsWith("<>c__"))
				{
					stack.Push("{ }");
					return null;
				}
				// see if there's a proxy class
				var proxyType = (from t in Assembly.GetExecutingAssembly().GetExportedTypes()
								 where t.GetCustomAttributes(typeof(JSProxyAttribute), false).Length > 0
								 let attr = t.GetCustomAttributes(typeof(JSProxyAttribute), false)[0] as JSProxyAttribute
								 where attr.Type == constructor.DeclaringType
								 select t)
								.FirstOrDefault();
				if (proxyType != null)
				{
					stack.Push(Activator.CreateInstance(proxyType, param));
					return null;
				}
				// see if this object can be ported directly to javascript
				if (constructor.DeclaringType.IsJSSafe())
				{
					stack.Push("new " + constructor.DeclaringType.Name + "(" + string.Join(", ", param.Select(p => p.ToString()).ToArray()) + ")");
					return null;
				}
				// push a .NET object onto the stack
				stack.Push(newObj = constructor.Invoke(param));
			}
			// array methods
			else if (inst.Code == OpCodes.Newarr)
			{
				//stack.Push(Array.CreateInstance((Type)inst.Operand, (int)stack.Pop()));
				int size = (int)stack.Pop();
				stack.Push("[ ]");
			}
			else if (inst.Code == OpCodes.Stelem_Ref)
			{
				object value = stack.Pop();
				object index = stack.Pop();
				object array = stack.Pop();
				return string.Format("{0}[{1}] = {2}", array, index, value);
			}
			else if (inst.Code == OpCodes.Ldelem_Ref)
			{
				object index = stack.Pop();
				object array = stack.Pop();
				stack.Push(string.Format("{0}[{1}]", array, index));
			}
			else if (inst.Code == OpCodes.Ldelema)
			{
				object index = stack.Pop();
				object array = stack.Pop();
				stack.Push(string.Format("{0}[{1}]", array, index));
			}
			else if (inst.Code == OpCodes.Stobj)
			{
				object value = stack.Pop();
				object target = stack.Pop();
				return target + " = " + value;
			}
			else if (inst.Code == OpCodes.Ldobj)
			{
				stack.Push(stack.Pop());
			}
			else if (inst.Code == OpCodes.Ldlen)
			{
				stack.Push(stack.Pop() + ".length");
			}
			// conversions
			else if (inst.Code == OpCodes.Box)
			{
				// javascript doesn't need boxing
			}
			else if (inst.Code == OpCodes.Castclass)
			{
				// javascript doesn't need to cast
			}
			else if (inst.Code == OpCodes.Conv_R4 || inst.Code == OpCodes.Conv_R8)
			{
				stack.Push("parseFloat(" + stack.Pop() + ")");
			}
			else if (inst.Code == OpCodes.Conv_I4)
			{
				stack.Push("parseInt(" + stack.Pop() + ")");
			}
			else if (inst.Code == OpCodes.Clt)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				stack.Push(b.ToString() + " < " + a);
			}
			else if (inst.Code == OpCodes.Cgt || inst.Code == OpCodes.Cgt_Un)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				stack.Push(b.ToString() + " > " + a);
			}
			else if (inst.Code == OpCodes.Ceq)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				stack.Push(b.ToString() + " == " + a);
			}
			else if (inst.Code == OpCodes.Constrained)
			{
				// i don't know what this is
			}
			else if (inst.Code.FlowControl == FlowControl.Branch)
			{
				// assume it's a return with a value
			}
			// branch means don't run the following code so use the opposite operator
			else if (inst.Code == OpCodes.Ble || inst.Code == OpCodes.Ble_S || inst.Code == OpCodes.Ble_Un || inst.Code == OpCodes.Ble_Un_S)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				return "if (" + b + " <= " + a + ")";
			}
			else if (inst.Code == OpCodes.Blt || inst.Code == OpCodes.Blt_S || inst.Code == OpCodes.Blt_Un || inst.Code == OpCodes.Blt_Un_S)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				return "if (" + b + " < " + a + ")";
			}
			else if (inst.Code == OpCodes.Bge || inst.Code == OpCodes.Bge_S || inst.Code == OpCodes.Bge_Un || inst.Code == OpCodes.Bge_Un_S)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				return "if (" + b + " >= " + a + ")";
			}
			else if (inst.Code == OpCodes.Bgt || inst.Code == OpCodes.Bgt_S || inst.Code == OpCodes.Bgt_Un || inst.Code == OpCodes.Bgt_Un_S)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				return "if (" + b + " > " + a + ")";
			}
			else if (inst.Code == OpCodes.Bne_Un || inst.Code == OpCodes.Bne_Un_S)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				return "if (" + b + " != " + a + ")";
			}
			else if (inst.Code == OpCodes.Beq || inst.Code == OpCodes.Beq_S)
			{
				var a = stack.Pop() ?? "null";
				var b = stack.Pop() ?? "null";
				return "if (" + b + " == " + a + ")";
			}
			else if (inst.Code == OpCodes.Brtrue || inst.Code == OpCodes.Brtrue_S)
			{
				var a = stack.Pop() ?? "null";
				return "if (" + a + " != null)";
			}
			else if (inst.Code == OpCodes.Brfalse || inst.Code == OpCodes.Brfalse_S)
			{
				var a = stack.Pop() ?? "null";
				return "if (" + a + " == false)";
			}
			else
				throw new NotImplementedException("The " + inst.Code.Name + " instruction was not handled.");

			return null;
		}

		private static string HandleMethod(Stack stack, ILInstruction inst)
		{
			MethodInfo method = (MethodInfo)inst.Operand;

			if (method.DeclaringType.IsJSSafe())
			{
				if (!method.IsStatic)
				{
					List<string> paramList = new List<string>();
					foreach (var i in Enumerable.Repeat(0, method.GetParameters().Length))
					{
						object param = stack.Pop();
						string paramStr = param is Delegate ? DissembleAction(param).ToString() : param.ToString();
						paramList.Add(paramStr);
					}
					paramList.Reverse();
					string paramListStr = string.Join(", ", paramList.ToArray());

					object called = stack.Pop();
					if (called.GetType().IsJSSafe())
						called = "this";
					else if (called is System.Web.UI.Page)
						called = "window";

					return called.ToString() + "." + method.Name + "(" + paramListStr + ")";
				}
				else
				{
					List<string> paramList = new List<string>();
					foreach (var i in Enumerable.Repeat(0, method.GetParameters().Length))
						paramList.Add(stack.Pop().ToString());
					string paramStr = string.Join(", ", paramList.ToArray());
					return "this." + method.Name + "(" + paramStr + ")";
				}
			}
			if (CallHandler.IsMethodHandled(method))
				return CallHandler.GetCode(method, stack);
			throw new NotImplementedException("The " + method.Name + " method on " + method.DeclaringType.Name + " was not handled.");
		}
	}
}
